package com.dysoft.flesh.ui;

import com.dysoft.bones.*;
import com.dysoft.bones.Event;
import com.dysoft.flesh.RenderContext;
import com.dysoft.flesh.RenderTarget;
import com.dysoft.flesh.Renderer;
import com.dysoft.flesh.gl.GLRenderer;
import com.dysoft.flesh.util.QuadTree;

import javax.media.opengl.GL;
import java.awt.*;
import java.awt.geom.Area;
import java.awt.geom.Rectangle2D;
import java.util.*;

/**
 * @author Sean Micklethwaite
 *         Oct 26, 2010 4:25:29 PM
 */
public abstract class UiCanvas extends UiContainer implements Renderer.Operation, Renderer, UiWidget.Container {
	final Root root;
	RenderTarget target;
	QuadTree<UiWidget> componentTree;
	Area repaintArea = null;

	EventChannel eventChannel;
	Set<UiWidget> mouseIn = null;
	boolean isQueued = false;

	final static Profiler PROFILER = new Profiler("UI Render");
	final static Profiler QUADTREE_PROFILER = new Profiler("UI QuadTree Query");

	public UiCanvas(Root root, RenderTarget target) {
		super(new Property.Trigger<Shape>(
				new Rectangle2D.Double(0, 0, target.getWidth(), target.getHeight())
		));
		this.root = root;
		this.target = target;
		componentTree = new QuadTree<UiWidget>(getShape().getBounds());
	}


	/**
	 * Forwards input events into this UI canvas
	 * @param eventChannel	Channel to receive events from
	 */
	public void bind(EventChannel eventChannel) {
		this.eventChannel = eventChannel;
		eventChannel.bind(new InputEvent.Mouse.Move(), new EventHandler( new EventHandler.Callback() {
			public Event.Status call(Event e, Event.Status status) {
				InputEvent.Mouse evt = (InputEvent.Mouse) e;
				Set<UiWidget> newMouseIn = query(evt.x, evt.y);

				if(mouseIn != null) for(UiWidget w : mouseIn) {
					if(!newMouseIn.contains(w)) {
						w.getState().onMouseOut(status);
					}
				}

				for(UiWidget w : newMouseIn) {
					Rectangle2D bounds = w.entry.getShape().getBounds();
					status = w.getState().onMouseOver(status, evt.x - (int)bounds.getMinX(),
							evt.y - (int)bounds.getMinY());
					if(status == Event.Status.STOPPED) break;
				}
				mouseIn = newMouseIn;
				return status;
			}
		}));
		eventChannel.bind(new InputEvent.Mouse.Up(), new EventHandler( new EventHandler.Callback() {
			public Event.Status call(Event e, Event.Status status) {
				InputEvent.Mouse evt = (InputEvent.Mouse) e;
				for(UiWidget w : query(evt.x, evt.y)) {
					Rectangle2D bounds = w.entry.getShape().getBounds();
					status = w.getState().onMouseUp(status, evt.x - (int)bounds.getMinX(),
							evt.y - (int)bounds.getMinY(), evt.button);

					if(status == Event.Status.STOPPED) break;
				}
				return status;
			}
		}));
		eventChannel.bind(new InputEvent.Mouse.Down(), new EventHandler( new EventHandler.Callback() {
			public Event.Status call(Event e, Event.Status status) {
				InputEvent.Mouse evt = (InputEvent.Mouse) e;
				for(UiWidget w : query(evt.x, evt.y)) {
					Rectangle2D bounds = w.entry.getShape().getBounds();
					status = w.getState().onMouseDown(status, evt.x - (int)bounds.getMinX(),
							evt.y - (int)bounds.getMinY(), evt.button);

					if(status == Event.Status.STOPPED) break;
				}
				return status;
			}
		}));
		eventChannel.bind(new InputEvent.Mouse.Scroll(), new EventHandler( new EventHandler.Callback() {
			public Event.Status call(Event e, Event.Status status) {
				InputEvent.Mouse.Scroll evt = (InputEvent.Mouse.Scroll) e;
				for(UiWidget w : query(evt.x, evt.y)) {
					Rectangle2D bounds = w.entry.getShape().getBounds();
					status = w.getState().onScroll(status, evt.x - (int)bounds.getMinX(),
							evt.y - (int)bounds.getMinY(), evt.amtx, evt.amty, evt.amtz);

					if(status == Event.Status.STOPPED) break;
				}
				return status;
			}
		}));
	}

	public Set<UiWidget> query(int x, int y) {
		Set<UiWidget> widgets = new HashSet<UiWidget>();
		QUADTREE_PROFILER.begin();
		componentTree.query(new Point(x,y), widgets);
		QUADTREE_PROFILER.end();
		return widgets;
	}

	@Override
	public UiWidget addWidget(final UiWidget widget) {
		if(widget.entry != null) widget.remove();

		widget.entry = componentTree.insert(widget, widget.getShape());

		if(widget instanceof Dynamic) {
			addDynamic(widget);
		}

		widget.onAdd(this);

		if(widget.monitor != null) {
			widget.shape.detach(widget.monitor);
		}
		widget.monitor = widget.shape.attach(new Trigger.Dependent() {
			public void update(Trigger cause) {
				//repaint(widget.entry.getShape().getBounds());
				widget.entry = widget.entry.update(widget.getShape());
				repaint();
			}
		});
		repaint();
		return widget;
	}

	public void removeWidget(UiWidget widget) {
		assert widget.entry != null;
		widget.entry.remove();
		widget.entry = null;
		repaint();
	}

	@Override
	public synchronized UiWidget addDynamic(UiWidget widget) {
		UiWidget r = super.addDynamic(widget);
		if(!isQueued) queue();
		return r;
	}

	public synchronized void repaint(Shape region) {
		if (repaintArea == null) {
			repaintArea = new Area(region);
			if(!isQueued) queue();
		} else {
			assert isQueued;
			repaintArea.add(new Area(region));
		}
	}

	public void repaint() {
		repaint(getShape());
	}

	public boolean call(RenderContext ctx) {
		Set<UiWidget> repaintSet = new HashSet<UiWidget>();

		UiRenderer.Context uictx = createContext(ctx, getShape(), new Point(0,0));

		if(repaintArea != null) {
			QUADTREE_PROFILER.begin();
			synchronized(this) {
				componentTree.query(repaintArea.getBounds(), repaintSet);
				repaintArea = null;
			}
			QUADTREE_PROFILER.end();
		}
		
		repaintSet.addAll(dynamicSet);

		if(!repaintSet.isEmpty()) {			
			PROFILER.begin();
			target.activate(ctx);

			// TODO: move this out
			((GLRenderer.GLRenderContext)ctx).getGL().glClear(GL.GL_COLOR_BUFFER_BIT);

			for(UiWidget c : repaintSet) {
				try {
					WidgetRenderer.renderWidget(uictx, c);
				} catch (DataManager.DataObject.NotPresentException e) {
					LOG.warn("Skipping widget + " + c, e);
				} catch (Throwable e) {
					LOG.error(e);
					e.printStackTrace(System.err);
				}
			}

			target.deactivate(ctx);
			PROFILER.end();
		}


		// Need to keep UiCanvas queued for render if we have dynamic widgets,
		// or if new repaint requests have arrived since we last rendered.

		synchronized (this) {
			isQueued = !dynamicSet.isEmpty() || repaintArea != null;
		}
		return isQueued;
	}

	public void render(RenderContext ctx) throws DataManager.DataObject.NotPresentException {
		call(ctx);
	}

	public EventChannel getEventChannel() {
		return eventChannel;
	}


	abstract protected Context createContext(RenderContext ctx, Shape shape, Point offset);


	//// PRIVATE ///////

	private void queue() {
		assert !isQueued;
		root.queuePreRenderOp(this);
		isQueued = true;
	}
}
